跳到主要内容

3.1-DOM和事件

Create by fall on 12 May 2020 Recently revised in 25 Apr 2024

DOM

文档对象模型 Document Object Model

浏览器中,document 指的是从 <html></html> 结束的部分,也是文档展示的部分

元素:element,一个典型的元素包含标签、文本、属性。我们一般会通过获取到元素(element)然后对 DOM 进行操作。

Element 继承了 Node 和 EventTarget 对象

<div id="foo" name="bar" class="cheer">
it's fall
</div>

在上方的内容中,节点类型为:

  • 标签节点(tag):<div></div>
  • 文本节点(text):it's fall 等文本内容
  • 属性节点(attribute):id="box1"

获取 element

  • 通过 id 获取对象:document.getElementById()
  • 通过 class,类名获取对象:document.getElementsByClass()
  • 通过 tag,节点类型获取对象:document.getElementsByTagName()
  • 通过 name,获取节点属性:document.getElementByName()
  • 通过选择器,获取第一个选择到的对象:document.querySelector()
  • 通过选择器,选择对象:document.querySelectorAll()

通过 id 和 querySelector 获得的是单个对象,其它的获取方式都是获取的列表

// 获取 id 为 foo 的节点
var domFoo = document.getElementById("foo")
// 获取 document,或者是其它节点内部的节点
var divList = document.getElementsByTagName("div")
// 获取 document,或者是其它节点内部的节点
var divCherrList = document.getElementsByClassName("cheer")
// 只能使用 document 进行获取
var nameBar = document.getElementByName("")

// 找到符合条件的第一个元素节点
var query = document.querySeletor("#foo.bar")
// 找到符合条件的所有元素节点
var queryList = document.querSeletorAll(".bar")

// 分别输出 id、类名、dom 设置的宽度
console.log(domFoo.id, domFoo.className, domFoo.style.width)
domFoo.id = "ssu"// 设置 id
domFoo.className = 'box4'// 设置
domFoo.style.width = '44px'// 给 CSS 对象中的高重新赋值

样式(style)

通过获取到元素的 DOM,可以对上面的样式进行修改

const dom = document.getElementById("foo")
// 样式名称和 CSS 中的基本相同
dom.style.height = '200px'
// 将 CSS 中的 - 转换为后面的字母大写即可
dom.style.backgroundColor = 'magenta'

在 js 中,style 属性只能访问和修改行内样式,即 body 内,访问不到 style 属性中的样式。如果 css 中使用 !important,会导致该样式无法通过 style 属性修改。

除了样式外,还提供了一些属性,这些属性是只读的

// 返回元素节点可见部分的宽高,一般为 content-box 的 width 和 height(带 padding)
Element.clientHeight
Element.clientWidth
// 返回元素节点左边框的宽度(border-left、border-top)返回整数
Element.clientLeft
Element.clientTop
// 返回元素节点包括滚动区域的高度和宽度
Element.scrollHeight
Element.scrollWidth
// 返回元素节点的向右滚动和向下滚动的像素数值,设置属性可以改变元素的滚动位置
Element.scrollLeft
Element.scrollTop
// 返回最靠近当前元素的、并且 CSS 的 position 属性不等于 static 的上层元素
Element.offsetParent
// 返回元素的垂直高度(包含 border,padding)
Element.offsetHeight
Element.offsetWidth
Element.offsetLeft// 返回当前元素左上角相对于 Element.offsetParent 节点的垂直偏移
Element.offsetTop //返回水平位移

计算后的样式(画面中渲染的样式)

属性(attribute)

<div class="foo" id="box"></div>
  • getAttribute:获取元素节点指定属性的值
  • setAttribute:创建或者改变元素节点的属性
  • removeAttribute:移除属性
// 通过 attribute 可以改变许多值
// 访问自定义名字
var oDiv = document.getElementById("id")
// 获取特定属性
oDiv.attributes["name"]
oDiv.getAttribute("id")
// 设置属性名
oDiv.setAttribute("title",'box1')
// 删除该属性,以及属性名
oDiv.removeAttribute("title")
// class 也可以通过 className 获取和修改
oDiv.className // classList 获取类名数组
// 获取大写的标签名
oDiv.tagName
// 元素的方向
oDiv.dir
// 元素是否可以拖动
oDiv.draggable
// 内部 HTML,可以修改
oDiv.innerHTML
// 整体 HTML,可修改
oDiv.outerHTML
// 是否设置了 contentEditable
oDiv.contentEditable

通用节点属性

当获取的 DOM 元素有很多子节点时,可以使用这些节点指针查找节点的附近节点

单个节点的指针

  • firstChild 元素子节点的第一个
  • lastChild 元素子节点的最后一个
  • previousSiblingnextSibling 当前节点的前一个节点、后一个节点(包括文本节点)
  • parentNode 当前节点的父节点
  • firstElementChildlastElementChild元素的第一个,最后一个元素节点
  • previousSiblingnextSibling 当前节点的前一个、后一个兄弟元素节点(包括文本节点)

多个节点指针

  • childNodes 获取元素子节点列表
  • children 获取不包括文本节点的所有节点

节点类型

nodeTypenodeNamenodeValue
元素节点1标签名null
属性节点2属性名属性值
文本节点3#text文本内容

其它 nodeType:注释 8 | 文档 9

文本节点:包括元素之间的空格,或者是回车在内的内容

getComputedStyle,全局方法,获取浏览器计算后,渲染的最终样式

// 获取 dom
const oDiv = document.querySelector("#foo")
console.log(oDiv.style)
// style 中的名称,是 css 中,将 - 删除,并将 - 后的首字母大写即可
// css 中的 background-color -> backgroundColor
getComputedStyle(oDiv)["height"]
// 将两个方法通过函数封装起来就可以实现兼容所有浏览器
function getStyle(node,cssStyle){
return node.currentStyle ? node.currentStyle['height'] : getComputedStyle(oDiv["height"])
}

获元素当前位置

  • getBoundingClientRect()
// 
const oDiv = document.querySelector("#foo")
oDiv.getBoundingClientRect()
// bottom: 203
// height: 34
// left: 863.5
// right: 1275.25
// top: 169
// width: 411.75
// x: 863.5
// y: 169

内容控制

通过 document 对象上的内容去创建和还被

  • document.createElement() 创建节点
  • document.createAttribute() 创建属性节点
  • document.createTextNode() 创建文本节点
// 根据标签名,创建对应的节点,该节点拥有对应的方法,和属性
const tag_A = document.createElement('a')
tag_A.link = 'http://www.google.com'
document.createAttribute()
// 创建 attribute
const attr = document.createAttribute("my_attrib")
attr.value = "newVal"
tag_A.setAttributeNode(a)
const text = document.createTextNode()

tag_A.appendChild( document.createTextNode('as we can') )

cloneNode 复制节点

let oDiv = document.querySelector(".classic")
let newNode = oDiv.cloneNode(true) // true 表示会深克隆所有节点后代

插入节点

  • appendChild(node) 向子节点的最后添加指定节点
  • append(string) 把 string 插入到文本末尾处
  • insertBefore(newNode,referenceNode) 当前节点前面插入节点

替换节点

  • replaceChild(newChild, oldChild) 用前者替换后者节点

删除节点

  • removeChild() 删除指定节点
  • remove() 删除节点自身
let oDiv = document.querySelector(".classic")
const tag_a = document.createElement('a')
const tag_span = document.createElement('span')
const tag_div = document.createElement('div')


tag_a.innerText = 'Fall'
tag_span.innerText = 'span 标签,知行合一'
tag_span.append('645645454')
tag_div.innerText = 'div 标签'
// 插入 a 标签
oDiv.appendChild(tag_a) // oDiv 最后插入 a 标签
oDiv.insertBefore(tag_span,tag_a) // 在 a 标签之前,插入 span
// 将 span 标签替换为 div 标签
oDiv.replaceChild(tag_div,tag_span)
// 删除 a 标签
oDiv.removeChild(tag_a)
// 或者使用 tag_a.remove() 移除自身

注意事项:

因为 JS 执行速度比对 DOM 操作执行的快很多。文档碎片操作(多次对 DOM 执行操作)时,可以通过一次性将文档插入到页面中的操作,进而节省 DOM 渲染时间,大多数框架会实现该内容。

document 的属性

  • 全局容器属性
    • document.documentElement.clientWidth:全局容器宽度
    • document.body.clientWidth:当前容器宽度
    • document.docuemtElement.scrollTop:当前容器顶端高度
    • document.body.scrollTop:当前容器顶端高度
oDiv.offsetleft
oDiv.offsetTop
// 获取当前可视区域距离第一个有定位的父节点的位置
getComputedStyle(oDiv)[width]
// 获取的仅为容器height设置的宽度

获取当前 document 容器的宽度

var width = document.documentElement.clientWidth;
var width = document.body.clientWidth;

const rectPosition = oDiv.getBoundingClientRect()
rectPosition.x // 距离最左侧 x 的距离等价于 left
rectPosition.y // 距离最顶部 y 的距离等价于 top
rectPosition.bottom // rect 最底部和 body 顶部的距离
rectPosition.right // rect 最右侧和 body 左侧的距离

特殊 DOM 属性

video

// 控制播放速率
$0.playbackRate = 2
// 暂停
$0.pause()
// 播放
$0.play()
// 循环控制
$0.loop = true

事件

Event(事件)

事件对象,所有的事件都继承了该对象

EventTarget(事件目标对象)Element,document,window 是最常见的事件目标

  • addEventListener() 添加事件
  • removeEventListener() 移除事件
  • dispatchEvent() 触发事件

任何事件目标都会实现与该接口有关的这三个方法。

const event = new Event("build");
// 监听该事件。
elem.addEventListener(
"build", (e) => {
/* … */
},
false,
);
// 分派该事件。
elem.dispatchEvent(event);

鼠标事件

MouseEvent(鼠标事件)

以下事件触发时,会调用 MouseEvent 事件

事件名称
click单击(鼠标按下,弹起整个过程结束后,触发点击事件)
dblclick双击
mouseover鼠标移入(移动到子节点上也会触发移出)
mouseout鼠标移出(移动到子节点上也会触发移出)
mousemove鼠标移动时触发(在当前 DOM 上移动时,会不停的触发)
mouseup鼠标抬起
mousedown鼠标按下
mouseenter鼠标移入(经过子节点不会重复触发)
mouseleave鼠标移出(经过子节点不会重复触发)
wheel鼠标的滚轮旋转
scroll滚动(内容区滚动的事件)(不算鼠标事件,为了和 wheel 对比,写在了这里

事件属性

function click(ev){
ev.button // 返回值 0,1,2,分别为鼠标左键滚轮和右键的点击
// 鼠标位置
ev.clientX
}

鼠标位置

  • clientXclinentY:在可视区域的位置(以 DOM 为基准的像素位置)
  • pageXpageY:当前页面的位置(滚轮滑动会改变值)
  • screenXscreenY:当前电脑显示器的横纵位置(窗口不是最大化的时候,移动会改变,以整个电脑屏幕为基准)
document.onmousedown = function(ev){
var e = ev || window.event;
alert(e.button);
}
var oDiv = document.getElementById('box');
oDiv.offsetLeft;

键盘事件

KeyboardEvent(键盘事件)

事件名称事件效果
keypress只有按字符键和方向键有效
keydown键盘按下时触发,并且长按功能键不会连续触发
keyup键盘抬起来时触发
  • shiftKeyaltKeyctrlKey(win)、metaKey(mac)如果按下对应按键,则值为 true
  • code 键码,区分是哪个按键(KeyA、ControlLeft)
  • key 按键的值,区分大小写,输出的值(a、Control、A)
  • 浏览器为了兼容还实现了一些其它键盘属性,但是不建议使用。
document.onclick = function(ev){
if(ev.ctrlKey){
console.log('ctrlyes')
}
}
window.onkeydown = function(ev){
var e = ev||window.event; //对获取方法进行兼容
var which = e.which||e.keyCode; //键码操作的兼容
alert(which)
}

触摸屏事件

事件名称事件效果
touchcancel中断 touch,例如:创建太多触点
touchstart一个或多个触点与触控设备表面接触时被触发。
touchend结束触碰时触发
touchmove触点在平面上移动的时候触发

属性

浏览器默认事件

事件名称事件效果
copy复制的时候触发(ctrl + c)
cut剪切的时候触发
contextmenu鼠标右键菜单事件

取消浏览器默认右键菜单

document.onload = function(){
document.oncontextmenu = function(){
return false;
}
}

阻止浏览器点击 a 链接时弹出窗口

function preDef(ev){
if(ev.preventDefault){
ev.preventDefault();
}
}

window 事件

  • load:当前页面加载完后触发
  • unload:当前页面解构的时候
  • scroll:页面滚动的时候触发
  • resize:窗口大小发生变化触发

document 事件

  • accept=".png, .jpg, .jpeg"
  • DOMContentLoaded 事件会在所有延迟加载的 JavaScript 脚本都执行完毕后触发。无需等待样式表、图像和子框架的完全加载。

  • visibilitychange DOM 的可见性更改时,将会触发该事件

document.addEventListener('DOMContentLoaded',function(){
console.log('DOMContentLoaded!');
});
for(var i=0; i<1000000000; i++){
// 这个同步脚本将延迟 DOM 的解析。
// 所以 DOMContentLoaded 事件稍后将启动。
}
// visibilitychange
addEventListener('visibilitychange',(ev)=>{
console.log(document.visibilityState)
if (document.hidden) {
audio.pause(); // 当窗口隐藏时,停止播放
} else {
audio.play();
}
})

element 事件

  • contextmenu 打开上下文菜单的时候触发
  • 鼠标事件
  • 键盘事件
  • 浏览器默认事件
  • scrollIntoView() 将当前元素滚动进入浏览器可见区域
const element = document.querySelector('.content')
element.scrollIntoView()

表单事件

form 表单事件

事件名称事件效果
blur失去焦点时触发
focus获取表单焦点时触发
focusout失去焦点触发
select输入框内文本选中时触发
change当对输入框内容进行修改,并且退出编辑时时触发
oDiv= document.getElementById('box')
oDiv.onchange = function(){
this.style.background = 'cyan'
}

必须添加在 form 元素上

  • submit,点击 <input type="submit"> 上的按钮才会触发
  • reset,点击 <input type="reset"> 按钮才会触发

获取 DOM 事件

getEventListeners:只有在调试的时候可以使用

const elementEvents = getEventListeners(element)
// 当前的点击事件
elementEvents.click

事件特性

target

触发对象是指 target 属性,只存在事件对象中,并且指向触发该事件的子容器

window.onclick = function(ev){
var e = ev||window.event;
var target = ev.target;
alert(target.innerHTML)
}

事件冒泡和事件捕获

  • 事件冒泡:触发该事件的时候,会像冒泡一样,逐渐向上,触发父节点的事件
  • 事件捕获:和事件冒泡相反,会从最外层的事件,逐渐触发,直到触发当前事件的 DOM

解决的方法

oli.onclick = function(ev){
ev.cancelBubble = true; // 取消事件冒泡
ev.stopPropagation(); // 方法阻止捕获和冒泡阶段中当前事件的进一步传播。
}

事件监听器

一般会通过事件监听器的方式添加事件,可以更精确的控制事件,并且附加的事件处理程序而不会覆盖已有的事件处理程序,因此可以为一个元素添加多个事件。

如果循环去添加一个匿名函数到事件监听器中,每一次都会添加新的事件,因此需要注意内存问题。或移除事件。

node.addEventListener(type,listener,options&useCapture)

第三个参数可以为 true 或者 false,可以表明

  • true(事件捕获):从父节点到子节点
  • false(事件冒泡):从子节点到父节点(默认)

也可以为一个配置的对象

  • capture 布尔值,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。
  • once 布尔值,表示 listener 在添加之后最多只调用一次。如果为 truelistener 会在其被调用之后自动移除。
  • passive 布尔值,设置为 true 时,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。
  • signal 可选 AbortSignal,该 AbortSignalabort() 方法被调用时,监听器会被移除。
// 示例
oBtn.addEventListener('click',handleClick,false)
function handleClick(ev){
console.log('点击了 button')
}

removeEventListener()

  • 第一个参数 事件类型
  • 第二个参数 删除的函数名称

只删除某一个函数而不影响其他的函数

oBtn.addEventListener('click',function(){
console.log('点击');
},false);
function show(){
console.log('强制')
}
oBtn.addEventListener('click',show,false);
oBtn.removeEcentListener('click',show);

事件委托

应用场景

如果使用 for 循环给节点添加事件,有一百个节点,使用一百个事件,浪费资源

实现步骤

  1. 找到当前节点的父节点或者祖先节点
  2. 将事件添加到父节点或者是祖先节点
  3. 找到触发对象,是否为想要的触发对象,然后进行操作
window.onload = function(){
var btn = document.getElementById('btn');
var list = document.getElementById('list');
var i = 0;
//点击则新增节点
btn.onclick = function(){
var nList = document.createElement('li');//每次循环都要新建一个节点
alert(i);
nList.innerHTML =((i++)+5)*1111;
list.appendChild(nList);
}
list.onclick =function(ev){
ev.target.style.backgroundColor = 'red';
}
}

dispatchEvent

用于触发事件,也可以是自定义事件

// 触发一次 input 标签上的事件
input.dispatchEvent(
// 第一个参数是触发事件的名称
new Event("input", {
bubbles: true,
cancelable: true
})
);

参考文章

作者链接
MDN 官方文档https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement
MDN 官方文档https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver
frontendmastershttps://frontendmasters.com/blog/dispatching-an-event/